package AFX::Process;

=head1 NAME

AFX::Process - Process Management for AFX

  Process::add_daemon()         - Add daemon for processing
  Process::add_machine()        - Add machine for processing
  Process::add_template()       - Add template for processing
  Process::check_daemon()       - Check is daemon mode in step specified for processing
  Process::DESTROY()            - Process destructor
  Process::execute()            - Execute process
  Process::execute_rcmd()       - Execute RCmd command
  Process::format_node_ref()    - Formatted Node with references
  Process::get_dash()           - Return Dash string
  Process::get_id()             - Return Process Identifier
  Process::get_list_machine()   - Return list of machines
  Process::get_list_template()  - Return list of templates
  Process::get_port()           - Return Port
  Process::get_site()           - Return Processing Site
  Process::get_thread_max()     - Return Max number of threads
  Process::get_thread_timeout() - Return Threads's timeout
  Process::is_debug()           - Is debug enabled
  Process::is_exclude()         - Is exclude mode enabled
  Process::message_critical()   - Message: Critical issue(s) detected
  Process::new()                - Process contructor
  Process::set_debug()          - Set debug mode
  Process::set_exclude()        - Set exclude mode
  Process::set_noprompt()       - Set noprompt for non-interactive (HTTP) mode
  Process::show_hosts()         - Show machine's hosts details
  Process::show_results()       - Show machine's ARF details

  proc_audit()                  - Process audit machine, executed in thread
  proc_rcmd()                   - Process rcmd on machine
  proc_scenario()               - Process Scenario on machine
  proc_timer()                  - Process monitor other threads for timeout

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2010 Mikhail Volkov

This file is part of AuditFX.

    AuditFX is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    AuditFX is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with AuditFX.  If not, see <http://www.gnu.org/licenses/>.
=cut

use strict;
use Carp;
use Exporter ();

use vars qw($VERSION);
my @ISA = qw(Exporter);
$VERSION = '1.0';

##############################################################################
# Globals
#

use threads 1.39;
use threads::shared;
use Thread::Queue;
use Storable qw(dclone);

use AFX::ARF;
use AFX::ATF;

## Flag to inform all threads that application is terminating
my $TERM : shared = 0;

## Flag to inform all threads that ASF updating
my $UPDATING : shared = 0;

##############################################################################
#
# Process::add_daemon()
#
# Add daemon for processing
#
# Return:
# - 1       - if added
# - 0       - otherwise
#
# Arguments expected are:
# - self    - Object reference
# - daemon  - Daemon mode

sub add_daemon {
    my ( $self, $daemon ) = @_;

## If Daemon not specified
    return 0 if !$daemon;

## Add Daemon into Process
    push(
        @{ $self->{_xml}->{daemon} },
        { state => "true", content => $daemon }
    );

## Return Ok, if daemon added
    return 1;

} ## end sub add_daemon

##############################################################################
#
# Process::add_machine()
#
# Add machine for processing
#
# Return:
# - 1         - if added
# - 0         - otherwise
#
# Arguments expected are:
# - self      - Object reference
# - units     - Unit's Identifiers
# - machines  - Machine's Identifiers

sub add_machine {
    my ( $self, $units, $machines ) = @_;

## Return Error, if no machines found in ASF
    return 0 if !$self->{ASF}->get_list_machine();

## Check machines, which will be processed
    foreach my $machine ( @{ $self->{ASF}->get_list_machine() } ) {

## Check machine's unit
        if ( $units and @{$units} ) {
            my @ids = grep { $_ eq $machine->{unit} } @{$units};
            next if !@ids;
        }

## Check machine's id
        if ( $machines and @{$machines} ) {
            my @ids = grep { $_ eq $machine->{id} } @{$machines};
            next if !@ids;
        }

## Add Machine into Process
        push(
            @{ $self->{_xml}->{machine} },
            { id => $machine->{id}, unit => $machine->{unit} }
        );
    }

## Return Ok, if machines added
    return 1 if $self->get_list_machine();

} ## end sub add_machine

##############################################################################
#
# Process::add_template()
#
# Add template for processing
#
# Return:
# - 1            - if added
# - 0            - otherwise
#
# Arguments expected are:
# - self         - Object reference
# - template_id  - ATF's Identifier

sub add_template {
    my ( $self, $template_id ) = @_;

## Add ATF into Process
    push( @{ $self->{_xml}->{template} }, { id => $template_id } );

## Return Ok, if template added
    return 1;

} ## end sub add_template

##############################################################################
#
# Process::check_daemon()
#
# Check is daemon mode in step specified for processing
#
# Return:
# - 1        - if specified or specified nothing
# - 0        - otherwise
#
# Arguments expected are:
# - self     - Object reference
# - step     - Step's Hash

sub check_daemon {
    my ( $self, $step ) = @_;

## Return Ok, if no daemon specified
    return 1 if !$self->{_xml}->{daemon};

## Return Error, if step's daemon not specified
    return 0 if !$step->{daemon};

## Define daemon
    my $daemon = $self->{_xml}->{daemon}->[0];

## Return Error, if no daily daemon, but daily specified
    return 0
      if $daemon->{content} eq 'daily'
          and $daemon->{content} ne $step->{daemon};

## Return Ok, if step's daemon mode equal to processing
    return 1;

} ## end sub check_daemon

##############################################################################
#
# Process::DESTROY()
#
# Process destructor
#
# Arguments expected are:
# - self         - Object reference

sub DESTROY {
    my ($self) = @_;

## Print debug message
    print "DEBUG::Process " . $self->get_id() . " destroyed\n"
      if $self->is_debug();

## Return Ok
    1;

} ## end sub DESTROY

##############################################################################
#
# Process::execute()
#
# Execute process
#
# Return:
# - 1           - if executed successfully
# - 0           - otherwise
#
# Arguments expected are:
# - self         - Object reference
# - templates    - Template's Identifiers

sub execute {
    my ( $self, $templates ) = @_;

## Set terminate flag
    $TERM = 0;

## Set ASF updating flag
    $UPDATING = 0;

## If templates specified
    if ($templates) {

## Check all specified templates
        foreach my $template ( @{$templates} ) {

## Add ATF for Processing
            $self->add_template($template);
        }
    }

## If not continue with hosts, return Error
    if ( !$self->show_hosts() ) {

## Return Error
        return 0;
    }

## If prompt required for interactive
    if ( !$self->{noprompt} ) {

## Gracefully terminate application on ^C or command line 'kill'
        $SIG{'INT'} = $SIG{'TERM'} = sub {

## Print Terminating message
            print "\n>>> P r o c e s s e s  t e r m i n a t i n g\n\n";

## Set terminate flag
            $TERM = 1;
        };    ## end SIG{'INT'}
    }

## Kill thread
    $SIG{'KILL'} = sub {

## Thread Detach
        threads->detach() if !threads->is_detached();

## Thread Exit
        threads->exit();

    };    ## end SIG{'KILL'}

## Workaround the threads "Alarm clock" bug
    $SIG{ALRM} = sub { };

## Create new Queue
    my $queue = Thread::Queue->new();

## Start timer thread
    threads->create( 'proc_timer', $queue );

## Processing Machine
    foreach my $proc_mach ( @{ $self->get_list_machine() } ) {

## If Terminate skip machine
        next if $TERM;

## Get machine from ASF
        my $machine =
          $self->{ASF}->get_machine( $proc_mach->{id}, $proc_mach->{unit} );

## Wait for any working threads+timer to finish
        sleep(1)
          while (
            $self->get_thread_max() - threads->list(threads::running) < 0 );

## If Terminate skip machine
        next if $TERM;

## Create new thread
        threads->create( "proc_$self->{id}", $queue, $self, $machine );

## Wait for 1 second
        sleep(1);
    }

## Define thread timeout
    my $thr_timeout = $self->get_thread_timeout();

## Wait for max timeout for threads to finish
    while ( threads->list(threads::running) > 0 and $thr_timeout + 60 ) {

## Set terminate flag, if only timeout thread is left
        $TERM = 1 if threads->list(threads::running) == 1;

## Decrement timeout
        $thr_timeout--;

## Wait for 1 second
        sleep(1);
    }

## Return Ok, if all process executed
    return 1;

} ## end sub execute

##############################################################################
#
# Process::execute_rcmd()
#
# Execute RCmd command
#
# Return: output
#
# Arguments expected are:
# - self         - Object reference
# - arf          - ARF Object
# - step_id      - Step's Number
# - login        - Login
# - rcmd         - Remote Command
# - scenario     - Scenario Object

sub execute_rcmd {
    my ( $self, $arf, $step_id, $login, $rcmd, $scenario ) = @_;

## Set step
    my $step = "";

## Set command's end
    my $end = "";

## Defined Output
    my $output = "";

## If noprompt command used
    if ( $rcmd =~ /$self->{_xml}->{noprompt}/ ) {

## Get prompt from ACF
        my $default_prompt = $self->{ACF}->get_prompt($login);

## Set user's prompt
        $end = $default_prompt->{content}
          if $default_prompt and $default_prompt->{content};
    }

## If scenario exists
    if ($scenario) {

## Get scenario's step
        my $scenario_step = $scenario->get_step($rcmd);

## Check if command is predefined
        $step = dclone($scenario_step) if $scenario_step;

## Add step ID into step
        $step->{id} .= "_$step_id" if $step;
    }

## Add ARF step
    $step = $arf->add_step( "RCMD_$step_id", $login, $rcmd, $end )
      if !$step;

## Return Error if step can't be added
    return if !$step;

## Set Step template if it not specified in ATF
    $step->{template} = $arf->{scenario};

## Print ATF completed
    $arf->message_step_processing( $step->{id} );

## Get command from ARF
    my $step_cmd = $arf->get_step_command( $step, 0, 0, 1 );

## Goto next command if command not specified
    return "" if !$step_cmd or !$step_cmd->{content};

## If no prompt mode wasn't specified
    if ( !$self->{noprompt} ) {

## Print ASF saving confirm
        print "\nCONFIRM EXECUTION '$step_cmd->{content}'?[Y/N] ";

## Require user input to continue
        chomp( my $yes = <STDIN> );

## Return error if No 
        return 0 if uc($yes) ne 'Y';
    }

## Check machines, which will be processed
    foreach my $machine ( @{ $arf->get_list_machine() } ) {

## Skip remote machines
        next if $machine->{remote};

## Check node in Machine
        foreach my $node ( @{ $machine->{node} } ) {

## If Terminate skip node
            next if $TERM;

## Skip if scenario not valid for machine
            next if $scenario and !$scenario->validate( $node->{host} );

## Add machine into ARF
            $arf->execute_step( $step, $node->{host} );

## Check all step's static
            foreach my $static ( @{ $step->{static} } ) {

## Skip static for parameters or N/A for node
                next if $static->{id} or $static->{content} =~ /^N\/A/;

## Goto next node if static not for this node
                next
                  if $static->{node} ne $node->{host}
                      and $static->{rnode} ne $node->{host};

## Get formatted node with references
                my $node_ref = $self->format_node_ref( $arf, $step, $static );

## Save and Print it
                print $node_ref;
                $output .= $node_ref;
            }
        }
    }

## Compare parameters between nodes
    $arf->compare_param(1);

## Parameters parsing in Step
    foreach my $param ( @{ $step->{param} } ) {

## Skip wrong parameter
        next if !$param->{node} or $param->{value};

## Skip referrer parameter
        next if $param->{severity} eq 'refer';

## Define rule representation
        my $param_hint = $arf->get_step_hint( $step, $param->{id} );

## Set hint
        my $hint = "No hint";

## Set hint as defined in step
        $hint = $param_hint->{content}
          if $param_hint and $param_hint->{content};

## Get Step's content
        my $param_warning .=
          "!!!WARNING!!! >> $param->{id} on $param->{node}: $hint <<\n\n";

## Save and print it
        print $param_warning;
        $output .= $param_warning;
    }

## Update ARF
    $arf->update();

## Return Output
    return $output;

} ## end sub execute_rcmd

##############################################################################
#
# Process::format_node_ref()
#
# Formatted Node with references
#
# Return:
# - formatted string
#
# Arguments expected are:
# - self    - Object reference
# - arf     - ARF Object reference
# - step    - Step's Hash
# - static  - Static's Hash

sub format_node_ref {
    my ( $self, $arf, $step, $static ) = @_;

## Format node title
    my $node_ref = "==>> " . $static->{node};

## Add remote node to node title
    $node_ref .= "&" . $static->{rnode} if $static->{rnode};

## If referer specified, add it to node
    $node_ref .= " [" . $static->{ref} . "]" if $static->{ref};

## Define content's variables as empty by default
    my $content = "No content";

## If static's content exists
    $content = $static->{content} if $static->{content} ne '';

## Get command from ARF
    my $step_cmd =
      $arf->get_step_command( $step, $static->{node}, $static->{rnode}, 1 );

## Add command before step
    $content = "$step_cmd->{content}\n\n$content\n\n" if $step_cmd;

## Print Step's content
    return "$node_ref: $content";

} ## end sub format_node_ref

##############################################################################
#
# Process::get_dash()
#
# Return Dash string
#
# Return:
# - dash string
#
# Arguments expected are:
# - self    - Object reference

sub get_dash {
    my ($self) = @_;

## Format dash line
    my $dash_me = "-";
    $dash_me .= "-" while ( length($dash_me) < 70 );

## Return Dash string
    return $dash_me . "\n";

} ## end sub get_dash

##############################################################################
#
# Process::get_id()
#
# Return Process Identifier
#
# Return:
# - identifier
#
# Arguments expected are:
# - self    - Object reference

sub get_id {
    my ($self) = @_;

## Return Identifier
    return $self->{_xml}->{id};

} ## end sub get_id

##############################################################################
#
# Process::get_list_machine()
#
# Return list of machines
# - 0             - if list empty
#
# Arguments expected are:
# - self          - Object reference

sub get_list_machine {
    my ($self) = @_;

## Return Machine list
    return $self->{_xml}->{machine} if $self->{_xml}->{machine};

## Return Error
    return 0;

} ## end sub get_list_machine

##############################################################################
#
# Process::get_list_template()
#
# Return list of templates
# - 0             - if list empty
#
# Arguments expected are:
# - self          - Object reference

sub get_list_template {
    my ($self) = @_;

## Return Machine list
    return $self->{_xml}->{template} if $self->{_xml}->{template};

## Return Error
    return 0;

} ## end sub get_list_template

##############################################################################
#
# Process::get_port()
#
# Return Port
#
# Return:
# - port
#
# Arguments expected are:
# - self    - Object reference

sub get_port {
    my ($self) = @_;

## Return Port
    return $self->{_xml}->{port} if $self->{_xml}->{port};

## Return error
    return 0;

} ## end sub get_port

##############################################################################
#
# Process::get_site()
#
# Return Processing Site
#
# Return:
# - site
#
# Arguments expected are:
# - self    - Object reference

sub get_site {
    my ($self) = @_;

## Return Version
    return $self->{ASF}->get_id();

} ## end sub get_site

##############################################################################
#
# Process::get_thread_max()
#
# Return Max number of threads
#
# Return:
# - max number
#
# Arguments expected are:
# - self    - Object reference

sub get_thread_max {
    my ($self) = @_;

## Return Max number
    return $self->{_thread}->{max} if $self->{_thread}->{max};

## Return error
    return 0;

} ## end sub get_thread_max

##############################################################################
#
# Process::get_thread_timeout()
#
# Return Threads's timeout
#
# Return:
# - max number
#
# Arguments expected are:
# - self    - Object reference

sub get_thread_timeout {
    my ($self) = @_;

## Return Max number
    return $self->{_thread}->{timeout} if $self->{_thread}->{timeout};

## Return error
    return 0;

} ## end sub get_thread_timeout

##############################################################################
#
# Process::is_debug()
#
# Is debug mode enabled
#
# Return:
# - 1       - if debug enabled
# - 0       - if debug disabled
#
# Arguments expected are:
# - self    - Object reference

sub is_debug {
    my ($self) = @_;

## Return error, if no debug specified
    return 0 if !$self->{_xml}->{debug};

## Return debug
    return 1 if $self->{_xml}->{debug}->[0]->{content} eq "true";

} ## end sub is_debug

##############################################################################
#
# Process::is_exclude()
#
# Is exclude mode enabled
#
# Return:
# - 1       - if exclude enabled
# - 0       - if exclude disabled
#
# Arguments expected are:
# - self    - Object reference

sub is_exclude {
    my ($self) = @_;

## Return error, if exclude not specified
    return 0 if !$self->{_xml}->{exclude};

## Return exclude
    return 1 if $self->{_xml}->{exclude}->[0]->{content} eq "true";

} ## end sub is_exclude

##############################################################################
#
# message_critical()
#
# Message: Critical issue(s) detected
#
# Return:
# - logged message
#
# Arguments expected are:
# - self         - Object reference
# - arf          - ARF Object

sub message_critical {
    my ( $self, $arf ) = @_;

## Set message
    my $message =
      "\n!!!WARNING!!! Critical issue(s) detected. Aborting scenario…\n";

## Return Message, if error already added into ARF
    return $message if !$arf->add_error($message);

## Return message
    return $self->{ACF}->logging( $message, 1, 'audit' );

} ## end sub message_critical

##############################################################################
#
# Process::new()
#
# Process contructor
#
# Arguments expected are:
# - ACF Object
# - ASF Object
# - Process's Identifier

sub new {

## Get package name
    my $type = shift;

## Define object's hash
    my $self = { ACF => shift, ASF => shift, id => shift, noprompt => 0 };

## Get Process from ACF
    $self->{_xml} = $self->{ACF}->get_process( $self->{id} );

## If process didn't specified in ACF or invalid
    if ( !$self->{_xml} or !$self->{_xml}->{thread} or !$self->{_xml}->{id} ) {

## Return if no process exists in ACF
        return 0;
    }

## Get first thread in Process
    $self->{_thread} = $self->{_xml}->{thread}->[0];

## Remove old logfiles and ARFs
    $self->{ACF}->purge($self);

## Print debug message
    print "DEBUG::Process " . get_id($self) . " created\n" if is_debug($self);

## Return object
    return bless $self, $type;

} ## end sub new

##############################################################################
#
# proc_audit()
#
# Process audit machine, executed in thread
#
# Command arguments expected are:
# - queue        - Registration queue
# - self         - Object reference
# - machine      - Machine hash from ASF

sub proc_audit {
    my ( $queue, $self, $machine ) = @_;

## My thread ID
    my $tid = threads->tid();

## Generate hash for ARF
    my $arf = new AFX::ARF( $self, $machine, 'audit' );

## Register with timer thread
    $queue->enqueue( $tid, $self->get_thread_timeout(), $arf->{id} );

## Print ARF processing message
    $arf->message_processing($tid);

## Define node's identifier
    my $node_id = $arf->get_node_id();

## Get Machine
    my $arf_machine = $arf->get_machine($node_id);

## ATF validation before audit
    foreach my $template ( @{ $arf_machine->{template} } ) {

## Open ATF
        my $atf = new AFX::ATF( $arf, $template->{id}, 'template' );

## Skip template if ATF not valid
        next if !$atf or !$atf->validate($node_id);

## Set updated state
        $arf->{machine_updated}++;

## Check ATF referrers
        $atf->validate_refer($node_id) if $atf;
    }

## If templates not specified
    if ( !$arf->{_process}->{template} ) {

## Check templates by type in Machine
        foreach my $atf_type ( 'HW', 'SCORE', 'DB', 'APP' ) {

## Skip if not templates in Machine
            next if !$arf_machine->{template};

## Check templates in Machine
            foreach my $template ( @{ $arf_machine->{template} } ) {

## Skip template with wrong type
                next
                  if !$template->{type}
                      or $template->{type} ne $atf_type;

## Add ATF for Processing
                push(
                    @{ $arf->{_process}->{template} },
                    { id => $template->{id} }
                );
            }
        } ## end types
    }

## Update ARF
    $arf->update();

## ATF processing
    foreach my $template ( @{ $arf->{_process}->{template} } ) {

## Open ATF
        my $atf = new AFX::ATF( $arf, $template->{id}, 'template' );

## Goto next template, if ATF invalid or not for this unit
        next if !$atf or !$atf->validate($node_id);

## Check steps in ATF
        foreach my $step ( @{ $atf->{_xml}->{step} } ) {

## Goto next step if step's daemon checking fail
            next if !$self->check_daemon($step);

## Set Step template if it not specified in ATF
            $step->{template} = $atf->get_id();

## Execute step for each node
            foreach my $node ( @{ $arf_machine->{node} } ) {

## Execute step for node
                $arf->execute_step( $step, $node->{host} );
            }
        }    ## step processing

## Print ATF completed
        $arf->message_atf_processed( $atf->get_id() );
    }

## Compare parameters between nodes
    $arf->compare_param();

## Create ARF
    $arf->create();

## If machine updated in ARF
    if ( $arf->{machine_updated} ) {

## Wait while ASF updating by another Thread
        sleep(1) while ( $UPDATING and !$TERM );

## Set Updating flag
        $UPDATING = 1;

## Open ASF
        my $asf = new AFX::ASF( $self->{ACF}, $self->get_site() );

## If ASF opened without errors
        if ($asf) {

## Check all machines in ARF
            foreach my $machine ( @{ $arf->{_xml}->{machine} } ) {

## Update machine
                $asf->update_machine($machine);
            }

## Update ASF
            $asf->update();
        }

## Set Updating flag
        $UPDATING = 0;
    }

## Print ARF processed message
    $arf->message_processed($tid);

## Destroy ARF
    $arf->DESTROY();

## Unregister with timer thread
    $queue->enqueue( $tid, undef, undef );

## Send KILL to thread
    threads->object($tid)->kill('KILL');

} ## end sub proc_audit

##############################################################################
#
# proc_rcmd()
#
# Process rcmd on machine
#
# Command arguments expected are:
# - self         - Object reference
# - login        - Login
# - templates    - Template's Identifiers

sub proc_rcmd {
    my ( $self, $login, $templates ) = @_;

## If not continue with hosts, return Error
    if ( !$self->show_hosts() or !$templates ) {

## Return Error
        return 0;
    }

## Set machines
    my $machines = $self->get_list_machine();

## Get machine from ASF
    my $machine_default =
      $self->{ASF}->get_machine( $machines->[0]->{id}, $machines->[0]->{unit} );

## Generate hash for ARF
    my $arf = new AFX::ARF( $self, $machine_default, 'rcmd', $templates->[0] );

## Gracefully terminate application on ^C or command line 'kill'
    $SIG{'INT'} = $SIG{'TERM'} = sub {

## Print Terminating message
        print "\n>>> T e r m i n a t i n g  a l l  s e s s i o n s\n\n";

## Set terminate flag
        $TERM = 1;

## Destroy ARF
        $arf->DESTROY();

## Print Terminating message
        print "\n>>> T e r m i n a t e d.\n\n";
    };    ## end SIG{'INT'}

## Print ARF processing message
    $arf->message_processing();

## Check machines, which will be processed
    foreach my $proc_mach ( @{$machines} ) {

## Get machine from ASF
        my $machine =
          $self->{ASF}->get_machine( $proc_mach->{id}, $proc_mach->{unit} );

## Check node in Machine
        foreach my $node ( @{ $machine->{node} } ) {

## Define as local machines
            my $remote = 0;

## Add machine into ARF
            $arf->add_machine( $machine, $node->{host}, $remote );
        }
    }

## Update ARF
    $arf->update();

## Open STF
    my $scenario = new AFX::ATF( $arf, $arf->{scenario}, 'scenario' );

## Set last command
    my $last_cmd = "whoami";

## Set all commands
    my @commands;

## For 999 steps
    for ( my $step_id = 1 ; $step_id < 999 ; $step_id++ ) {

## Print Help
        print
"\n### Exit: q; Help: ?[XXX]; Repeat last cmd: /; Last cmd: $last_cmd";

## Print rcmd prompt for non-root
        print "\n$login@" . "rcmd> ";

## Require user input to continue
        chomp( my $rcmd = <STDIN> );

## Remove non-interested ASCII symbols
        $rcmd =~ s/[^\x0A\x20-\x7E]//g;

## Remove spaces and new lines in the start
        $rcmd =~ s/^[\s\n]+//g;

## Remove spaces and new lines at the end
        $rcmd =~ s/[;\s\n]+$//g;

## Exit if command not specified
        last if $rcmd eq 'quit' or $rcmd eq 'q';

## Skip if command not specified
        next if !$rcmd or $rcmd =~ /^\s*$/;

## If command as /X
        if ( $rcmd =~ /^\/(\d+)/ ) {

## If \0 specified
            if ( $1 == 0 ) {

## Create ARF
                print
"\nARF $arf->{directory}/$arf->{id}.html generated successfully\n"
                  if $arf->create();

## Goto next command
                next;
            }

## If command not found
            if ( !$commands[ $1 - 1 ] ) {

## Print error message
                print "\nGet Help/Command_Listing (?[XXX]) first\n";

## Goto next command
                next;
            }

## Set commands
            $rcmd = $commands[ $1 - 1 ];
        }

## Execute last command
        if ( $rcmd eq '/' ) {

## Set last command
            $rcmd = $last_cmd;
        }
## Show all sessions
        elsif ( $rcmd eq '/?' ) {

## Print header
            print sprintf( "\n"
                  . $self->get_dash()
                  . "[PID] %-22s\t%-20s\n"
                  . $self->get_dash(),
                "SESSION-ID/ERROR", "LOG-FILE" );

## Return Error, if no machines found
            if ( !$arf->get_list_session() ) {

## Print no sessions
                print "No opened sessions detected\n"
                  . $self->get_dash() . "\n";

## Goto next command
                next;
            }

## Define error sessions
            my $session_err = 0;

## Show open sessions
            foreach my $session ( @{ $arf->get_list_session() } ) {

## Print session description
                print sprintf( "[%d] %-22s\t%-20s",
                    $session->{_pid}, $session->{id}, $session->{logfile} );

## If session with error
                if ( $session->{_error} ) {

## Print session's error
                    print "\n$session->{_error}";

## Increment error sessions
                    $session_err++;
                }

## Print line after session
                print "\n" . $self->get_dash();
            }

## Print Sessions count
            print "\n"
              . scalar( @{ $arf->get_list_session() } )
              . " opened session(s); $session_err session(s) with error\n";

## Goto next command
            next;
        }
        elsif ( $rcmd =~ '^\?' ) {

## Goto next if scenario not found
            next if !$scenario;

## Set empty commands
            @commands = ();

## Remove ? from rcmd
            $rcmd =~ s/^\?//;

## Print Help: Show all RCMD scenarios
            print "\n@? - Show all RCMD scenarios (STF)\n";

## Print Help: Load RCMD scenario
            print "\n\@STF - Load RCMD scenario\n";

## Print Help: /? content
            print "\n\/? - Show all opened sessions\n";

## Print Help: /0 content
            print "\n\/0 - Create ARF (Audit Result File)\n";

## Check steps in STF
            foreach my $step ( @{ $scenario->{_xml}->{step} } ) {

## Goto next step
                next if $rcmd and eval { $step->{id} !~ /$rcmd/ };

## Define rule representation
                my $param_hint = $arf->get_step_hint($step);

## Set hint
                my $hint = "No hint";

## Set hint as defined in step
                $hint = $param_hint->{content}
                  if $param_hint and $param_hint->{content};

## Push command
                push( @commands, $step->{id} );

## Print Step's content
                print "\n\/" . scalar(@commands) . " $step->{id} - $hint\n";
            }

## Goto next command
            next;
        }
        elsif ( $rcmd =~ '^\@\?' ) {

## Find STF
            my $stfnames = $self->{ACF}->find_atf( 'scenario', "RCMD.*" );

## Check if scenario exist
            next if !$stfnames;

## Check templates
            foreach my $stfname ( @{$stfnames} ) {

## Open STF
                my $stf = new AFX::ATF( $arf, $stfname, 'scenario' );

## if STF invalid
                next if !$stf;

## Get Title
                my $title = $stf->get_title();

## If title isn't specified
                if ( !$title or !$title->{content} ) {
                    $title = "No Title";
                }
                else {
                    $title = $title->{content};
                }

## Print Scenario's and title
                print "\n" . $stf->get_id() . " - " . $title . "\n";
            }

## Goto next command
            next;
        }
        elsif ( $rcmd =~ '^\@(\S+)' ) {

## Open STF
            $scenario = new AFX::ATF( $arf, $1, 'scenario' );

## If scenario opened
            if ($scenario) {

## Set new template
                $arf->{scenario} = $1;
            }
            else {

## Open Old STF
                $scenario = new AFX::ATF( $arf, $arf->{scenario}, 'scenario' );
            }

## Goto next command
            next;
        }
        else {

## Set last command
            $last_cmd = $rcmd;
        }

## Execute RCmd
        $self->execute_rcmd( $arf, $step_id, $login, $rcmd, $scenario );

## Discard terminate flag
        $TERM = 0;
    }

## Create ARF
    $arf->create();

## Print ARF processed message
    $arf->message_processed();

## Destroy ARF
    $arf->DESTROY();

} ## end sub proc_rcmd

##############################################################################
#
# proc_scenario()
#
# Process Scenario on machine
#
# Command arguments expected are:
# - queue        - Registration queue
# - self         - Object reference
# - machine      - Machine hash from ASF

sub proc_scenario {
    my ( $queue, $self, $machine ) = @_;

## My thread ID
    my $tid = threads->tid();

## Register with timer thread
    $queue->enqueue( $tid, $self->get_thread_timeout(), $machine->{id} );

## Check scenarios
    foreach my $template ( @{ $self->get_list_template() } ) {

## Generate hash for ARF
        my $arf = new AFX::ARF( $self, $machine, 'scenario', $template->{id} );

## Print ARF processing message
        $arf->message_processing($tid);

## Get Machine
        my $arf_machine = $arf->get_machine( $arf->get_node_id() );

## Update ARF
        $arf->update();

## Open ATF
        my $scenario = new AFX::ATF( $arf, $template->{id}, 'scenario' );

## Skip if scenario not valid for machine
        next if !$scenario or !$scenario->validate( $arf->get_node_id() );

## Set param errors in step flag
        my $param_critical = 0;

## Check steps in STF
        foreach my $step ( @{ $scenario->{_xml}->{step} } ) {

## Set Step template if it not specified in ATF
            $step->{template} = $scenario->get_id();

## Print ATF completed
            $arf->message_step_processing( $step->{id} );

## Execute step for each node
            foreach my $node ( @{ $arf_machine->{node} } ) {

## Execute step for node
                $arf->execute_step( $step, $node->{host} );

## Check all step's static
                foreach my $static ( @{ $step->{static} } ) {

## Skip static for parameters or N/A for node
                    next if $static->{id} or $static->{content} =~ /^N\/A/;

## Goto next node if static not for this node
                    next
                      if $static->{node} ne $node->{host}
                          and $static->{rnode} ne $node->{host};

## Print formatted node with references
                    print $self->format_node_ref( $arf, $step, $static );
                }

## Check all parameters
                foreach my $param ( @{ $step->{param} } ) {

## Skip wrong parameter
                    next if !$param->{node} or $param->{value};

## Skip referrer parameter
                    next if $param->{severity} ne 'critical';

## Add critical error
                    $param_critical++;

## Add message into ARF
                    $self->message_critical($arf);
                }
            }

## Update ARF
            $arf->update();

## Last if critical parameter found
            last if $param_critical;
        }    ## step processing

## Check processed steps
        foreach my $step ( @{ $arf->{_xml}->{step} } ) {

## Skip wrong step's templates
            next
              if $step->{template} eq $template->{id}
                  or !$self->is_exclude();

## Set review flag as false
            $step->{review} = 'false';
        }

## Compare parameters between nodes
        $arf->compare_param();

## Create ARF
        $arf->create();

## Print ARF processed message
        $arf->message_processed($tid);

## Destroy ARF
        $arf->DESTROY();
    }

## Unregister with timer thread
    $queue->enqueue( $tid, undef, undef );

## Send KILL to thread
    threads->object($tid)->kill('KILL');

} ## end sub proc_scenario

##############################################################################
#
# proc_timer()
#
# Process monitor other threads for timeout
#
# Command arguments expected are:
# - queue - Registration queue

sub proc_timer {
    my ($queue) = @_;

## My thread ID
    my $qtid = threads->tid();

## Contains threads, timeout and machine
    my %timers;

## Loop until told to quit
    while ( !$TERM or %timers ) {

## Check queue
        while ( my $tid = $queue->dequeue_nb() ) {

## Set timeout
            $timers{$tid}{'timeout'} = $queue->dequeue();

## Set Machine's id
            $timers{$tid}{'machine'} = $queue->dequeue();

## Set thread object
            $timers{$tid}{'thread'} = threads->object($tid);

## Check thread's timeout and object
            if ( !$timers{$tid}{'timeout'} or !$timers{$tid}{'thread'} ) {

## If timeout not specified, unregister thread
                delete( $timers{$tid} );
            }
        }

## Cancel timed out threads
        foreach my $tid ( keys(%timers) ) {

## Decrement timeout
            $timers{$tid}{'timeout'}--;

## Check timeout and terminate flag
            if ( $timers{$tid}{'timeout'} < 0 or $TERM ) {

## Send KILL to thread
                $timers{$tid}{'thread'}->kill('KILL');

## Print Killed message
                print
">>> $timers{$tid}{'machine'}: thread session timed out. Killed.\n";

## Delete thread from timers
                delete( $timers{$tid} );
            }
        }

## Wait for 1 second
        sleep(1);
    }

## Send KILL to thread
    threads->object($qtid)->kill('KILL');

} ## end sub proc_timer

##############################################################################
#
# Process::set_debug()
#
# Set debug mode
#
# Arguments expected are:
# - self     - Object reference

sub set_debug {
    my ($self) = @_;

## Set debug mode
    $self->{_xml}->{debug}->[0]->{content} = "true";

## Return Ok
    return 1;

} ## end sub set_debug

##############################################################################
#
# Process::set_exclude()
#
# Set exclude mode
#
# Arguments expected are:
# - self     - Object reference

sub set_exclude {
    my ($self) = @_;

## Set exclude mode
    $self->{_xml}->{exclude}->[0]->{content} = "true";

## Return Ok
    return 1;

} ## end sub set_exclude

##############################################################################
#
# Process::set_noprompt()
#
# Set noprompt for non-interactive (HTTP) mode
#
# Arguments expected are:
# - self     - Object reference

sub set_noprompt {
    my ($self) = @_;

## Set noprompt mode
    $self->{noprompt} = 1;

## Return Ok
    return 1;

} ## end sub set_noprompt

##############################################################################
#
# Process::show_hosts()
#
# Show machine's hosts details
#
# Return:
# - 1         - if continue and hosts exists
# - 0         - otherwise
#
# Command arguments expected are:
# - self      - Object reference

sub show_hosts {
    my ($self) = @_;

## Print header
    print sprintf( "\nValidating connections. Please wait...\n"
          . $self->get_dash()
          . "\t%-22s\t%-20s\t%s\n"
          . $self->get_dash(),
        "HOST-ID", "HOST-IP", "TERMINAL" );

## Return Error, if no machines found
    if ( !$self->get_list_machine() ) {

## Print no sessions
        print "Machines not available\n" . $self->get_dash() . "\n";

## Return Error
        return 0;
    }

## Define hosts flag
    my $hosts = 0;

## Show Machines, which will be processed
    foreach my $proc_mach ( @{ $self->get_list_machine() } ) {

## Get machine from ASF
        my $machine =
          $self->{ASF}->get_machine( $proc_mach->{id}, $proc_mach->{unit} );

## Print Machine with unit
        print $machine->{id} . " [" . $machine->{unit} . "]\n";

## Create new ARF Object
        my $arf = new AFX::ARF( $self, $machine, 'audit' );

## Get Machine
        my $arf_machine = $arf->get_machine( $arf->get_node_id() );

## Check all nodes
        foreach my $node ( @{ $arf_machine->{node} } ) {

## Get terminal
            my $node_terminal = $self->{ASF}->get_node_terminal($node);

## Print host description
            print sprintf( "\t%-22s\t%-20s\t%s\n",
                $node->{host}, $node->{ip}, $node_terminal->{id} );

## Increment hosts flag
            $hosts++;

## Mastering Machine
            $arf->mastering_machine( $node->{host} );
        }

## Check passwords specified in Machine
        foreach my $mach_passwd ( @{ $arf_machine->{passwd} } ) {

## Goto next password, if password specified
            next if $mach_passwd->{content};

## Require user input to continue
            print "PLEASE ENTER PASSWORD FOR $mach_passwd->{login}: ";
            chomp( my $new_passwd = <STDIN> );

## Skip empty password
            next if !$new_passwd;

## Open Keyfile
            my $cbc = $self->{ACF}->open_keyfile();

## Set machine's password and unit
            $mach_passwd->{content}   = $new_passwd;
            $mach_passwd->{encrypted} = "false";
            $mach_passwd->{unit}      = $arf_machine->{unit};

## Try to encrypt password
            if ($cbc) {

## Encrypt password
                $mach_passwd->{content} =
                  $cbc->encrypt_hex( $mach_passwd->{content} );

## Set encryption state
                $mach_passwd->{encrypted} = "true";
            }

## Set machine updated flag
            $arf->{machine_updated}++;

## Add ACF password
            $self->{ACF}->add_passwd($mach_passwd);
        }

## If machine updated in ARF
        if ( $arf->{machine_updated} ) {

## Update machine
            $self->{ASF}->update_machine($arf_machine);
        }

## Destroy ARF
        $arf->DESTROY();
    }

## If no hosts found
    if ( !$hosts ) {

## Print hosts not available
        print "Hosts not available\n" . $self->get_dash() . "\n";

## Return error
        return 0;
    }

## Print Dash ended line
    print $self->get_dash() . "\n";

## Update ASF
    $self->{ASF}->update() if $self->{ASF}->is_updated();

## If noprompt specified
    return 1 if $self->{noprompt};

## Require user input to continue
    print "DO YOU WANT TO PROCEED?[Y/N] ";
    chomp( my $yes = <STDIN> );

## Return input status
    return uc($yes) eq 'Y';

} ## end sub show_hosts

##############################################################################
#
# Process::show_results()
#
# Show ARF details
#
# Return:
# - 1         - if error or failed parameters exists
# - 0         - otherwise
#
# Command arguments expected are:
# - self      - Object reference

sub show_results {
    my ($self) = @_;

## Define status flag
    my $proc_status = 0;

## Print header
    print sprintf(
        "\n" . $self->get_dash() . "\t%-32s\t%-20s\n" . $self->get_dash(),
        "TEMPLATE-ID", "STATUS [FAILED PARAM]" );

## Return Error, if no machines found
    if ( !$self->get_list_machine() ) {

## Print no sessions
        print "Machines not available\n" . $self->get_dash() . "\n";

## Return Error
        return 1;
    }

## Show Machines, which will be processed
    foreach my $proc_mach ( @{ $self->get_list_machine() } ) {

## Get machine from ASF
        my $machine =
          $self->{ASF}->get_machine( $proc_mach->{id}, $proc_mach->{unit} );

## Print Machine with unit
        print $machine->{id} . " [" . $machine->{unit} . "]\n";

## Create new ARF Object
        my $arf = new AFX::ARF( $self, $machine, 'audit' );

## If opening ARF failed, next
        if ( !$arf or !-f $arf->{path} ) {

## Print machine's details
            print sprintf("\tARF not available\n");

## Add error for status
            $proc_status++;

## Goto next machine
            next;
        }

## Open ARF
        $arf->open();

## Get initial node's Identifier
        my $node_id = $arf->get_node_id();

## Get Machine
        my $arf_machine = $arf->get_machine($node_id);

## Check processing errors
        if ( $arf->{_xml}->{error} and @{ $arf->{_xml}->{error} } ) {
            $proc_status++;
        }

## Get ARF statistics
        my ( $arf_param, $failed, $passed ) = $arf->get_statistics();

## Print no ATF found for machine
        if (   !$arf_machine->{template}
            or !@{ $arf_machine->{template} } )
        {
## Print no ATF message
            print "\tNo ATF for Machine validated\n";

## Add no templates as process status
            $proc_status++;
        }

## Show Templates in Machine
        foreach my $template ( @{ $arf->{_process}->{template} } ) {

## Set default template status
            my $status = "PASSED";

## If failed parameters
            if ( $failed->{ $template->{id} } ) {
                $status = "FAILED [$failed->{$template->{id}}]";

## Add failed templates as process status
                $proc_status++;
            }

## If no passed parameters
            if ( !$passed->{ $template->{id} } ) {
                $status = "N/A";
            }

## Skip template if it not validated
            next
              if !$arf->get_machine_template( $node_id, $template->{id} );

## Print template's details
            print sprintf( "\t%-32s\t%-20s\n", $template->{id}, $status );
        }

## Set default machine's parameters
        my $mach_param = "Failed: $arf_param->{error}; ";
        $mach_param .= "Passed: $arf_param->{ok}; N/A: $arf_param->{na}";

## Print machine's details
        print sprintf( "\t%-22s\t%-20s\n", "> PARAMETERS <", $mach_param );

## Add failed parameters as process status
        $proc_status += $arf_param->{error};

## Add process status if no ok parameters
        $proc_status++ if !$arf_param->{ok};

## Destroy ARF
        $arf->DESTROY();
    }

## Print Dash ended line
    print $self->get_dash() . "\n";

## Return error if failed parameters or processing error exists
    return 1 if $proc_status;

## Return Ok
    return 0;

} ## end sub show_results

1;
